lwIP更新记01:全局互斥锁替代消息机制

您所在的位置:网站首页 lwip 线程安全 lwIP更新记01:全局互斥锁替代消息机制

lwIP更新记01:全局互斥锁替代消息机制

2023-06-03 08:49| 来源: 网络整理| 查看: 265

从 lwIP-2.0.0 开始,在 opt.h 中多了一个宏开关 LWIP_TCPIP_CORE_LOCKING,默认使能。这个宏用于启用 内核锁定 功能,使用 全局互斥锁 实现。在之前,lwIP 使用 消息机制 解决 lwIP 内核线程安全问题。消息机制易于实现,因为实现简单,更容易做到高稳定性。但使用消息机制效率低下,所以 lwIP 从 V2.0.0 开始,正式支持(并默认使用)全局互斥锁,同时仍保留消息机制。全局互斥锁方案实现复杂,但效率非常高。 本文探索全局互斥锁方案是怎么诞生并发展的。

2007 年 5 月 24 日,Simon Goldschmid (以下简称 西蒙 )提交了一个任务:task #6935(使用当前 socket/netconn API 需要解决的问题)。

西蒙提出,当前的 socket/netconn API 还存在某些设计问题需要改进,比如:

1.将消息机制改为互斥(锁定 netconns)

move from message passing to mutual exclusion (lock netconns)

这里需要解释一下 消息机制 是什么。

在使用操作系统的情况下,lwIP 内核作为操作系统的一个任务( tchip_thread )运行,内核线程 和 应用程序线程 间的通讯是通过消息机制来完成的。

在当时,距离发布 lwIP-1.3.0 版本还有 10 个月,在这个版本中,如果 应用程序线程 使用 sendto 函数发送 UDP 数据,这是一个复杂的过程。实际上应用程序线程无权发送数据,它会先构造一个称为 TCPIP_MSG_API 的消息,连带发送的数据信息,投递给 内核线程,由内核线程进行实际的数据发送:

请添加图片描述

从图中可以看出,一个普通的数据发送,需要两个线程协同、进行两次任务切换才能完成。多年以来,lwIP 对外声称都是”为了获得良好的性能,你应该使用 raw API“。这就是原因!因为线程间通讯低效造成的。

为什么要让内核线程完成实际的发送?直接在 应用程序线程 中发送不可以吗?

不可以!

这涉及到多年以来 lwIP 对外声称的另一个注意事项:“lwIP 不是线程安全的”!!!

从 lwip 诞生以来,直到最近发布的 lwIP-2.1.x,以及可以看到的将来,lwIP 都不是线程安全的。

所以 lwIP 采用了一个迂回的策略,用来满足多线程编程:使用 并发设计 中的 队列模式。

举一个很好理解的例子。

如果有两个任务使用同一个 串口 打印调试信息,任务 A 打印:

printf("Task A: ABCDEFG\n");

任务 B 打印:

printf("Task B: 1234567\n");

任务A正在打印的时候,可能被任务B打断,这时任务A的打印信息就会显得不完整,任务 A 打印的信息被任务 B 打印的信息分成两部分:

Task A: ABTask B: 1234567 CDEFG

出现这个问题的原因是有多个任务同时使用一个资源,那么让任务不去同时访问资源,更进一步,干脆让任务不访问资源不就解决了吗。队列模式 就是这个思路,即所有任务都不直接使用串口资源,而是将要打印的信息放到一个队列中,串口发送也独立出一个任务,该任务从队列中取出信息,打印信息。完美解决大家争资源的问题。

lwIP 处理 线程不安全 也是用的这个思路:创建一个邮箱队列,投递消息相当于入队,内核线程 从邮箱队列中取出消息,根据消息的指示做出相应的动作,所有 应用程序线程 都不访问 lwIP 内核,也就避免了线程不安全的问题。

除了一个问题外,效率低。

西蒙萌生出了改变这一现状的想法:

对于发送,我们可以锁定整个核心,而不是将消息传递给另一个线程。这样 TX 可以优先处理。锁定互斥锁的运行时间不应比消息传递长多少。对于接收,同样可以锁定整个核心,这样 tchip_thread 只需要处理 RX 和 定时器 。

随着讨论的进行,大家觉得使用全局互斥锁是个可行的方案。

Frédéric Bernon (以下简称 佛雷德里克)根据这个思路修改了代码,并进行了测试。他的测试代码是用 sendto 函数发送 1458 字节的数据(UDP 连接),测试结果如下:

Sequential API: lwip_sendto()raw API: udp_sendto使用消息机制204 us74 us使用全局互斥量67 us74 us

可以看出,使用全局互斥量后:

lwip_sendto 函数用时减少了 3 倍与 udp_sendto 函数执行时间相比,仅慢了 3us 。

可以说效果拔群。 佛雷德里克 是怎么做到的呢?原因在于他使用了全局互斥量让发送数据成了 原子操作 ,从而避免了线程间的切换,实现了在 应用程序线程 中调用 udp_sendto 函数直接完成数据的发送:

请添加图片描述

由于少了消息传递和任务切换的开销,速度自然也就快了起来。

这种使用互斥量保护共享资源的操作,使用了 并发设计 中的 守卫调用模式。如果多个线程以某种方式同时调用相互干扰的服务集合,可以使用守卫调用模式使得多个线程串行访问服务。最常见的实现方式是使用互斥量给共享资源加锁。

继续用上面的例子,有两个任务使用同一个 串口 打印调试信息,则可能出现打印乱套的问题。如果换一种方式,任务 A 在使用串口前,先把串口锁定,一旦锁定,除非任务 A 主动解锁,否则其它任务都不能使用串口。这样就能避免任务 A 正在使用串口打印信息时,被任务 B 打断的现象了。

2007 年 6 月 9 日, 佛雷德里克 将这个更改推送到了 lwIP 代码仓库,这是历史上第一次,宏 LWIP_TCPIP_CORE_LOCKING 出现在 lwIP 的源码中,默认设置为 0 ,即不启用该新功能。

但此时的代码还是实验性质的,仅供 lwIP 开发者测试用,在这个宏开关的注释中,有着明显的提示:

/* EXPERIMENTAL, Don't use it if you're not an active lwIP project member */

实验性质,如果你不是 lwIP 项目的开发人员,请不要使用它

之后,使用互斥量 锁定内核 的操作方式不断完善:

2007 年 7 月底,API 消息(TCPIP_MSG_API)已经几乎完全可以使用锁定内核的方式替换掉,这包括连接、发送等操作2010 年 2 月份,西蒙 第一次引入宏 LWIP_TCPIP_CORE_LOCKING_INPUT,底层数据包输入消息(TCPIP_MSG_INPKT)也可以选择使用锁定内核的方式了。当然,这依旧是实验性质的。2016 年 3 月 17,终于, LWIP_TCPIP_CORE_LOCKING 和 LWIP_TCPIP_CORE_LOCKING_INPUT 不再是实验性质的,它们正式向用户开放了!从第一次讨论“将消息机制改为互斥”,到最终推出这个功能,用了将近 10 年!2016 年 7 月 1,宏 LWIP_TCPIP_CORE_LOCKING 的默认值修改为 1 ,这意味着解决 “lwIP 不是线程安全的”的方案, lwIP 开发者更推荐使用互斥锁。2016 年 11 月 10, lwIP-2.0.0 发布,在这个版本中,我们第一次可以放心使用互斥锁方案。

时代变了。“为了获得良好的性能,你应该使用 raw API” 这一印象,从 lwIP-2.0.0 开始,要改写了!从此,使用 Sequential 或 socket API 也可以达到接近 raw API 的性能!

有关操作系统移植层的信息,可以参考我的这篇博文。

读后有收获,资助博主养娃 - 千金难买知识,但可以买好多奶粉 (〃‘▽’〃) 千金难买知识,但可以买好多奶粉



【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3